package com.actor.networking;

import com.esotericsoftware.kryo.KryoException;
import com.esotericsoftware.kryo.Registration;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import com.esotericsoftware.kryo.util.DefaultClassResolver;
import static com.esotericsoftware.kryo.util.DefaultClassResolver.NAME;
import com.esotericsoftware.kryo.util.IdentityObjectIntMap;
import com.esotericsoftware.kryo.util.IntMap;
import com.esotericsoftware.kryo.util.ObjectMap;
import static com.esotericsoftware.kryo.util.Util.className;
import static com.esotericsoftware.minlog.Log.TRACE;
import static com.esotericsoftware.minlog.Log.trace;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import org.objectweb.asm.ClassReader;

/**
 * @author Dmitry
 */
public class KryoClassResolver extends DefaultClassResolver {
    private final Map<Class, Integer> classHashCodeMap = new HashMap<>();
    private final Map<Class, Class> errorShownForClassMap = new HashMap<>();

    private int getClassHashCode(Class clazz) throws IOException {
        int hash;
        if (classHashCodeMap.get(clazz) != null) {
            hash = classHashCodeMap.get(clazz);
        } else {
            ClassReader classReader = new ClassReader(clazz.getName());
            hash = Arrays.hashCode(classReader.b);
            FileOutputStream f = new FileOutputStream(new File("f:/2.txt"));
            f.write(classReader.b);
            f.close();
            classHashCodeMap.put(clazz, hash);
        }
        return hash;
    }

    private void showClassesNotEqualError(Class clazz) {
        if (!errorShownForClassMap.containsKey(clazz)) {
            errorShownForClassMap.put(clazz, clazz);
            System.err.println("Deserialized class from remote message is different comparing to local class " + clazz.getName());
        }
    }

    @Override
    protected void writeName(Output output, Class type, Registration registration) {
        try {
            output.writeVarInt(NAME + 2, true);
            if (classToNameId != null) {
                int nameId = classToNameId.get(type, -1);
                if (nameId != -1) {
                    if (TRACE) {
                        trace("kryo", "Write class name reference " + nameId + ": " + className(type));
                    }

                    output.writeVarInt(nameId, true);
                    return;
                }
            }

            // Only write the class name the first time encountered in object graph.
            if (TRACE) {
                trace("kryo", "Write class name: " + className(type));
            }

            int nameId = nextNameId++;
            if (classToNameId == null) {
                classToNameId = new IdentityObjectIntMap();
            }

            classToNameId.put(type, nameId);
            output.writeVarInt(nameId, true);
            output.writeString(type.getName());
            output.writeInt(getClassHashCode(type));
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }

    @Override
    protected Registration readName(Input input) {
        int nameId = input.readVarInt(true);
        if (nameIdToClass == null) {
            nameIdToClass = new IntMap();
        }
        Class type = nameIdToClass.get(nameId);
        if (type == null) {
            // Only read the class name the first time encountered in object graph.
            String className = input.readString();
            int storedClassHashCode = input.readInt();
            type = getTypeByName(className);
            if (type == null) {
                try {
                    type = Class.forName(className, false, kryo.getClassLoader());
                    int classHashCode = getClassHashCode(type);
                    if (classHashCode != storedClassHashCode) {
                        showClassesNotEqualError(type);
                    }
                } catch (ClassNotFoundException ex) {
                    throw new KryoException("Unable to find class: " + className, ex);
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
                if (nameToClass == null) {
                    nameToClass = new ObjectMap();
                }
                nameToClass.put(className, type);
            }
            nameIdToClass.put(nameId, type);
            if (TRACE) {
                trace("kryo", "Read class name: " + className);
            }
        } else {
            if (TRACE) {
                trace("kryo", "Read class name reference " + nameId + ": " + className(type));
            }
        }
        return kryo.getRegistration(type);
    }
}
